/*
 * Copyright © 2017 eqxiu.com 北京中网易企秀科技有限公司  All rights reserved.
 */
package cn.hermit.data.xml.ns;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import javax.xml.XMLConstants;

import cn.hermit.data.xml.model.XmlNameNode;
import cn.hermit.util.ReflectionUtils;

/**
 * NamespaceContext to handle name spaces and prefixes in XML Document tree.
 * 
 * @author Jack Gao (Chinese name : GAO JIANGUO, Email : linux.gjg@gmail.com)
 * @date 1 Jul, 2014
 */
public final class NamespaceContext implements javax.xml.namespace.NamespaceContext {

    private Map<String, String>[] nsContext;

    private int pointer;

    int nsLayer;

    @SuppressWarnings("unchecked")
    public NamespaceContext() {
        nsContext = new Map[3];
    }

    @Override
    public String getNamespaceURI(String prefix) {
        if (pointer == 0 || nsLayer >= pointer) {
            return null;
        }
        for (int i = nsLayer; i > -1; i--) {
            Map<String, String> nsMap = nsContext[i];
            String ns = null;
            if ((ns = nsMap.get(prefix)) != null) {
                return ns;
            }
        }
        return XMLConstants.NULL_NS_URI;
    }

    @Override
    public String getPrefix(String namespaceURI) {
        if (namespaceURI == null || pointer == 0 || nsLayer >= pointer)
            return null;
        for (int i = nsLayer; i > -1; i--) {
            Map<String, String> nsMap = nsContext[i];
            for (Entry<String, String> entry : nsMap.entrySet()) {
                if (namespaceURI.equals(entry.getValue())) {
                    return entry.getKey();
                }
            }
        }
        return XMLConstants.DEFAULT_NS_PREFIX;
    }

    @Override
    public Iterator<String> getPrefixes(String namespaceURI) {
        if (namespaceURI == null || pointer == 0 || nsLayer >= pointer)
            return null;
        List<String> prefixes = new ArrayList<String>();
        for (int i = nsLayer; i > -1; i--) {
            Map<String, String> nsMap = nsContext[i];
            for (Entry<String, String> entry : nsMap.entrySet()) {
                if (namespaceURI.equals(entry.getValue())) {
                    prefixes.add(entry.getKey());
                }
            }
        }
        return prefixes.iterator();
    }

    /**
     * Load name space definitions to context.
     * 
     * @param key
     * 		the key of name space definition
     * @param value
     * 		the name space
     * @param incrementLayer
     * 		If true, will consider the document tree go in deep with one step
     * @return
     * 		If loaded a new name space definition
     */
    public boolean loadNamespace(String key, String value, boolean incrementLayer) {
        if (value == null || !XmlNameNode.isNamespaceName(key)) {
            return false;
        }
        Map<String, String> nsMap = null;
        if (incrementLayer) {
            nsMap = new LinkedHashMap<String, String>();
            if (pointer == nsContext.length) {
                resizeStack(nsContext.length * 2);
            }
            nsLayer = pointer;
            nsContext[pointer++] = nsMap;
        } else {
            nsMap = nsContext[pointer - 1];
        }
        String prefix = XmlNameNode.getNamespacePrefix(key);
        nsMap.put(prefix, value);
        return true;
    }

    /**
     * Load name space definitions to context.
     * 
     * @param attributes
     * 		Attributes which may be a name space definition
     * @param incrementLayer
     * 		If true, will consider the document tree go in deep with one step
     * @return
     * 		If loaded a new name space definition
     */
    public boolean loadNamespaces(Map<String, String> attributes, boolean incrementLayer) {
        if (attributes.size() == 0) {
            return false;
        }
        Map<String, String> nsMap = null;
        if (incrementLayer) {
            nsMap = new LinkedHashMap<String, String>();
            if (pointer == nsContext.length) {
                resizeStack(nsContext.length * 2);
            }
            nsLayer = pointer;
            nsContext[pointer++] = nsMap;
        } else {
            nsMap = nsContext[pointer - 1];
        }
        boolean hasNSDef = false;
        for (String key : attributes.keySet()) {
            if (XmlNameNode.isNamespaceName(key)) {
                String prefix = XmlNameNode.getNamespacePrefix(key);
                nsMap.put(prefix, attributes.get(key));
                hasNSDef = true;
            }
        }
        if (ReflectionUtils.isEmpty(nsMap)) {
            nsContext[--pointer] = null;
            nsLayer--;
            return false;
        }
        return hasNSDef;
    }

    /**
     * Unload name spaces which defined in element with the given QName.
     * 
     * @param name
     * 		The given QName
     * @return
     * 		If unload name spaces successful
     */
    public boolean unloadNamespaces(QName name) {
        if (name == null || name.nsLayer != pointer - 1 || pointer <= 0) {
            return false;
        }
        nsContext[--pointer] = null;
        nsLayer = name.nsLayer - 1;
        return true;
    }

    /**
     * Unload name spaces by layer in XML document tree.
     * 
     * @param nsLayer
     * 		Layer in XML document tree
     * @return
     * 		If unload name spaces successful
     */
    public boolean unloadNamespaces(int nsLayer) {
        if (nsLayer != pointer - 1 || pointer <= 0) {
            return false;
        }
        nsContext[--pointer] = null;
        this.nsLayer = nsLayer - 1;
        return true;
    }

    @SuppressWarnings({ "unchecked" })
    private void resizeStack(int newCapacity) {
        Map<String, String>[] newStack = new Map[newCapacity];
        System.arraycopy(nsContext, 0, newStack, 0, Math.min(pointer, newCapacity));
        nsContext = newStack;
    }

    /**
     * Get QName from name space context by tag
     * 
     * @param tag
     * 		The tag
     * @return
     * 		A new QName generated from context by tag
     */
    public QName getQName(String tag) {
        Entry<String, String> nameInfo = XmlNameNode.getNameInfo(tag);
        String ns = getNamespaceURI(nameInfo.getKey());
        if (ns != null) {
            QName name = new QName(ns, nameInfo.getValue(), nameInfo.getKey());
            name.namespaceContext = this;
            name.nsLayer = nsLayer;
            return name;
        }
        int index = tag.indexOf(':');
        String prefix = XMLConstants.DEFAULT_NS_PREFIX;
        String localPart = null;
        if (index == -1) {
            localPart = tag;
        } else {
            prefix = tag.substring(0, index);
            if (XMLConstants.XMLNS_ATTRIBUTE.equals(prefix)) {
                localPart = prefix;
                prefix = tag.substring(index + 1);
            } else {
                localPart = tag.substring(index + 1);
            }
        }
        return new QName(XMLConstants.NULL_NS_URI, localPart, prefix);
    }

    /**
     * Get name spaces in current layer when XML parser walks in XML document tree.
     * 
     * @return
     * 		Name spaces in current layer 
     */
    public Map<String, String> getCurrentNamespaces() {
        if (pointer == 0) {
            return Collections.emptyMap();
        }
        return nsContext[pointer - 1];
    }

    /**
     * Get current layer number when XML parser walks in XML document tree.
     * 
     * @return
     * 		Current layer number
     */
    public int getCurrentLayer() {
        return pointer - 1;
    }

    /**
     * Get name spaces from context by layer of document tree.
     * 
     * @param nsLayer
     * 		The layer of document tree
     * @return
     * 		Name spaces with prefixes
     */
    public Map<String, String> getNamespaces(int nsLayer) {
        if (nsLayer < 0 || nsLayer >= pointer)
            return Collections.emptyMap();
        return nsContext[nsLayer];
    }

    /**
     * Get name spaces from context by current layer of document tree.
     * 
     * @return
     * 		Name spaces with prefixes
     */
    @SuppressWarnings("unchecked")
    public Map<String, String>[] getNamespaces() {
        if (nsLayer >= pointer)
            return null;
        Map<String, String>[] nsMaps = new Map[nsLayer + 1];
        System.arraycopy(nsContext, 0, nsMaps, 0, nsMaps.length);
        return nsMaps;
    }

    /**
     * Check if context has defined name space with prefix and URI.
     * 
     * @param prefix
     * 		Prefix of name space
     * @param namespaceURI
     * 		URI of name space
     * @return
     * 		
     */
    public boolean hasDefinedNS(String prefix, String namespaceURI) {
        if (namespaceURI == null || prefix == null)
            return false;
        if (pointer == 0) {
            return false;
        }
        for (int i = nsLayer; i > -1; i--) {
            Map<String, String> nsMap = nsContext[i];
            if (nsMap == null)
                continue;
            for (Entry<String, String> entry : nsMap.entrySet()) {
                if (prefix.equals(entry.getKey()) && namespaceURI.equals(entry.getValue())) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Check if context has defined name space with URI.
     * 
     * @param namespaceURI
     * 		URI of name space
     * @return
     */
    public boolean hasDefinedNS(String namespaceURI) {
        if (namespaceURI == null)
            return false;
        if (pointer == 0) {
            return false;
        }
        for (int i = nsLayer; i > -1; i--) {
            Map<String, String> nsMap = nsContext[i];
            if (nsMap == null)
                continue;
            for (Entry<String, String> entry : nsMap.entrySet()) {
                if (namespaceURI.equals(entry.getValue())) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Destroy the name space context.
     */
    public void destory() {
        for (int i = nsContext.length - 1; i > -1; i--) {
            if (nsContext[i] != null) {
                nsContext[i].clear();
                nsContext[i] = null;
            }
        }
    }

}